앨범 사이트 만들기

✒️ 2025-05-26 11:06 내용 수정


실습 목표

실습 흐름

  1. 데이터베이스에 테이블 및 시퀀스, 필요 시 샘플 데이터 추가
  2. 데이터베이스에 연결 : context.xml 파일, 라이브러리(JDBC)
  3. 테이블의 정보를 저장할 DTO 클래스 생성
  4. 데이터베이스에서 조회, 추가, 삭제를 수행하는 DAO 클래스 생성
  5. 사진을 등록, 삭제, 다운로드할 JSP, 사진 추가 형식의 JSP 생성
  6. DAO 클래스 객체를 생성하고, JSP와 연결할 Servlet 생성
  7. Servlet에서 코드 실행 시 결과 확인

DB에 테이블 추가

--시퀀스
CREATE SEQUENCE SEQ_PHOTO_IDX;

--테이블
CREATE TABLE PHOTO(
	IDX NUMBER(3) PRIMARY KEY,
	TITLE VARCHAR2(100),
	FILENAME VARCHAR2(100),
	PWD VARCHAR2(50),
	IP VARCHAR2(20),
	REGIDATE DATE
);

DB 연결

  1. context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
	<Resource 
	        auth="Container" 
      		name="jdbc/oracle_test"
      		type="javax.sql.DataSource"
      		driverClassName="oracle.jdbc.driver.OracleDriver"
      		factory="org.apache.commons.dbcp.BasicDataSourceFactory"
      		url="jdbc:oracle:thin:@localhost:1521:xe"
      		username="계정명" password="비밀번호" 
      		maxActive="20" maxIdle="10" maxWait="1"/>
</Context>
  1. 라이브러리
파일
commons-collections-3.2.1.jar
commons-dbcp-1.2.2.jar
commons-pool-1.4.jar
ojdbc8-23.3.0.23.09.jar
cos.jar
lombok.jar
  1. service용 DBService.java
package service;

import java.sql.Connection;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

public class DBConnection {

	static DBConnection db = null;
	DataSource ds;

	// singleton pattern으로 생성
	public static DBConnection getInstance() {
		if(db == null) {
			db = new DBConnection();
		}
		return db;
	}

	// 생성자에서 Context 객체와 DataSource 초기화
	public DBConnection() {
		try {
			InitialContext ic = new InitialContext();
			Context ctx = (Context)ic.lookup("java:comp/env");
			ds = (DataSource)ctx.lookup("jdbc/oracle_test");
		
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}

	// 생성자에서 준비한 정보로 DB에 연결하여 Connection 객체 얻기
	public Connection getConnection() {
		Connection connec = null;
		try {
			connec = ds.getConnection();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return connec;
	}
}

Ajax

  1. HttpRequest.js
var xhr = null;

function createRequest() {
	if (xhr != null) {
		return;
	}
	if (window.ActiveXObject) {
		xhr = new ActiveXObject("Microsoft.XMLHTTP"); // IE 환경
	} else {
		xhr = new XMLHttpRequest(); // 기타 브라우저 환경
	}
}

function sendRequest(url, param, callback, method) {

	// HttpRequest 생성
	createRequest();

	// 전송 타입 구분
	var httpMethod = (method != 'POST' && method != 'post') ? 'GET' : 'POST';

	// 파라미터 구분
	var httpParam = (param == null || param == '') ? null : param;

	// 접근 url
	var httpURL = url;

	// 요청 방식이 GET이고 전달할 파라미터가 있다면 새 url 경로 제작
	if (httpMethod == 'GET' && httpParam != null) {
		httpURL = httpURL+'?'+httParam;
	}

	// 서버로 보낼 Ajax 요청 형식
	xhr.open(httpMethod, httpURL, true);

	// requestHeader 설정 : Content-Type 지정
	xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

	// 작업이 완료된 후 호출할 callback 메소드 지정
	xhr.onreadystatechange = callback;

	// Ajax 요청을 서버로 전달
	xhr.send(httpMethod == 'POST' ? httpParam : null);
}

DTO와 DAO

  1. dto
package dto;

public class PhotoDTO {
	private int idx;
	private String title;
	private String filename;
	private String pwd;
	private String ip;
	private String regidate;
	
	public int getIdx() {
		return idx;
	}
	public void setIdx(int idx) {
		this.idx = idx;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getFilename() {
		return filename;
	}
	public void setFilename(String filename) {
		this.filename = filename;
	}
	public String getPwd() {
		return pwd;
	}
	public void setPwd(String pwd) {
		this.pwd = pwd;
	}
	public String getIp() {
		return ip;
	}
	public void setIp(String ip) {
		this.ip = ip;
	}
	public String getRegidate() {
		return regidate;
	}
	public void setRegidate(String regidate) {
		this.regidate = regidate;
	}
}
package dto;

import lombok.Data;

@Data
public class PhotoDTO {
	private int idx;
	private String title;
	private String filename;
	private String pwd;
	private String ip;
	private String date;
}
  1. dao
package dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import dto.PhotoDTO;
import service.DBService;

public class PhotoDAO {
	
	static PhotoDAO single = null;
	// singleton pattern
	public static PhotoDAO getInstance() {
		if (single == null) {
			single = new PhotoDAO();
		}
		return single;
	}
	
	// 사진 전체 조회
	public List<PhotoDTO> selectList() {
		
		List<PhotoDTO> list = new ArrayList<PhotoDTO>();
		Connection con = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		String sql = "SELECT * FROM PHOTO";
		
		try {
			// 1. Connection 객체를 얻어온다
			con = DBService.getInstance().getConnection();
			
			// 2. 명령 처리 객체 정보를 얻어온다
			pstmt = con.prepareStatement(sql);
			
			// 3. 결과행 처리 객체를 얻어온다
			rs = pstmt.executeQuery();
			
			while(rs.next()) {
				PhotoDTO dto = new PhotoDTO();
				dto.setIdx(rs.getInt("idx"));
				dto.setTitle(rs.getString("title"));
				dto.setFilename(rs.getString("filename"));
				dto.setPwd(rs.getString("pwd"));
				dto.setIp(rs.getString("ip"));
				dto.setRegidate(rs.getString("regidate"));
				
				list.add(dto);
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if(rs != null) {
					rs.close();
				}
				if(pstmt != null) {
					pstmt.close();
				}
				if(con != null) {
					con.close();
				}
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}
		return list;
	}
	
	// 사진 등록
	public int insert(PhotoDTO dto) {
		int res = 0;
		
		Connection con = null;
		PreparedStatement pstmt = null;
		
		String sql = "INSERT INTO PHOTO VALUES(SEQ_PHOTO_IDX.nextVal, ?, ?, ?, ?, SYSDATE)";
		
		try {
			// 1. Connection 획득
			con = DBService.getInstance().getConnection();
			
			// 2. 명령 처리 객체 획득
			pstmt = con.prepareStatement(sql);
			
			// 3. pstmt에 parameter 채우기
			pstmt.setString(1, dto.getTitle());
			pstmt.setString(2, dto.getFilename());
			pstmt.setString(3, dto.getPwd());
			pstmt.setString(4, dto.getIp());
			
			// 4. DB로 데이터 전송 (res : 처리된 행 수)
			res = pstmt.executeUpdate();
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if(pstmt != null) {
					pstmt.close();
				}
				if(con != null) {
					con.close();
				}
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}
		return res;
	}
	
	// 사진 제거
	public int delete(int idx) {
		int res = 0;
		
		Connection con = null;
		PreparedStatement pstmt = null;
		
		String sql = "DELETE FROM PHOTO WHERE IDX=?";
		
		try {
			// 1. Connection 획득
			con = DBService.getInstance().getConnection();
			
			// 2. 명령 처리 객체 획득
			pstmt = con.prepareStatement(sql);
			
			// 3. pstmt에 parameter 채우기
			pstmt.setInt(1, idx);
			
			// 4. DB로 데이터 전송 (res : 처리된 행 수)
			res = pstmt.executeUpdate();
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if(pstmt != null) {
					pstmt.close();
				}
				if(con != null) {
					con.close();
				}
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}
		return res;
	}
	
}

Servlet

  1. file list
package action;

import java.io.IOException;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.PhotoDAO;
import dto.PhotoDTO;

@WebServlet("/list")
public class PhotoListAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// DAO로 DB에 접근해서 photo 전체 목록 조회
		List<PhotoDTO> list = PhotoDAO.getInstance().selectList();
		
		// list를 request 영역에 바인딩
		request.setAttribute("list", list);
		
		// request 영역에 바인딩 된 list를 어떤 jsp에서 사용할건지 포워딩
		RequestDispatcher disp = request.getRequestDispatcher("photo_list.jsp");
		disp.forward(request, response);
	}

}
  1. file insert
package action;

import java.io.File;
import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

import dao.PhotoDAO;
import dto.PhotoDTO;


@WebServlet("/insert")
public class PhotoInsertAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// 경로 설정
		String web_path = "/upload/";
		ServletContext application = request.getServletContext();
		String path = application.getRealPath(web_path);
		System.out.println("real path : " + path);
		
		// 파일 사이즈 설정 : 100mb
		int max_size = 1024 * 1024 * 100;
		
		// MultipartRequest 객체 생성
		MultipartRequest mr = new MultipartRequest(request, path, max_size, "utf-8", new DefaultFileRenamePolicy());
		
		// 파일 이름 설정
		String fileName = "";
		File f = mr.getFile("photo"); // 전달받은 파일 검색
		
		if (f != null) {
			fileName = f.getName();
		}
		
		String title = mr.getParameter("title");
		String pwd = mr.getParameter("pwd");
		String ip = request.getRemoteAddr();
		
		// 객체에 정보 담기
		PhotoDTO dto = new PhotoDTO();
		dto.setTitle(title);
		dto.setFilename(fileName);
		dto.setPwd(pwd);
		dto.setIp(ip);
		
		int res = PhotoDAO.getInstance().insert(dto);
		
		if (res > 0) {
			response.sendRedirect("list");
		}
	}

}
  1. file delete
package action;

import java.io.File;
import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.PhotoDAO;

@WebServlet("/delete")
public class PhotoDeleteAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// delete?idx=n&filename=n
		request.setCharacterEncoding("utf-8");
		
		int idx = Integer.parseInt(request.getParameter("idx"));
		String fileName = request.getParameter("filename");
		
		// 서버의 /upload/ 경로 가져오기
		String web_path = "/upload/";
		ServletContext application = request.getServletContext();
		String path = application.getRealPath(web_path);
		
		// DB에서 파일 정보 삭제
		int res = PhotoDAO.getInstance().delete(idx);
		
		String param = "no";
		
		if(res > 0) {
			File f = new File(path, fileName);
			if (f.exists()) {
				f.delete();
			}
			param = "yes";
		}
		
		// 결과값인 param을 json 구조로 포장하여 출력 텍스트 작성
		String resultStr = String.format("[{'param':'%s'}]", param);
		response.getWriter().print(resultStr);

	}

}
  1. file download
package action;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/download")
public class PhotoDownloadAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// 서버에 파일이 저장된 경로 가져오기
		String dir = request.getParameter("dir");
		String fullPath = request.getServletContext().getRealPath(dir);
		
		String fileName = "";
		fileName = request.getParameter("filename");
		String fullPathName = String.format("%s%s", fullPath, fileName);
		
		// 파일 가져오기
		File file = new File(fullPathName);
		byte[] b = new byte[1024*1024*100]; // 파일 최대 한도만큼 바이트 배열 설정
		
		// 사용자 브라우저 타입 얻어오기
		String strAgent = request.getHeader("User-Agent");
		System.out.println("browser : " + strAgent);
		
		String userCharset = request.getCharacterEncoding();
		if (userCharset == null) {
			userCharset = "utf-8";
		}

		String value = "";
		// IE인 경우
		// IE 지원이 종료되어 향후엔 IE 부분이 필요 없을지도 모른다.
		if (strAgent.indexOf("MSIE") > -1) {
			// IE 5.5 인 경우
			if (strAgent.indexOf("MSIE 5.5") > -1) {
				value = "filename="+fileName;
			} else if (strAgent.indexOf("MSIE 7.0") > -1) { // IE 7.0인 경우
				// 인코딩 타입 비교
				if (userCharset.equalsIgnoreCase("UTF-8")) {
					fileName = URLEncoder.encode(fileName, userCharset);
					fileName = fileName.replaceAll("\\+", " ");
					value = "attachment; filename=\""+fileName+"\"";
				} else {
					value = "attachment; filename=" + new String(fileName.getBytes(userCharset), "ISO-8859-1"); 
				}
			} else { // IE 8.0 이상인 경우 두 번 호출됨
				// 인코딩 타입 비교
				if (userCharset.equalsIgnoreCase("UTF-8")) {
					fileName = URLEncoder.encode(fileName, userCharset);
					fileName = fileName.replaceAll("\\+", " ");
					value = "attachment; filename=\""+fileName+"\"";
				} else {
					value = "attachment; filename" + new String(fileName.getBytes(userCharset), "ISO-8859-1");
				}
			} // IE 확인 종료
		} else if (strAgent.indexOf("Firefox") > -1) { // Firefox인 경우
			// Firefox는 공백 문자 이후가 인식되지 않음
			// 다만 기타 브라우저랑 코드는 같은데..
			value = "attachment; filename=" + new String(fileName.getBytes(), "ISO-8859-1");
		} else { // 그외 브라우저
			value = "attachment; filename=" + new String(fileName.getBytes(), "ISO-8859-1");
		}
		
		// 브라우저가 캐싱하지 않도록 설정
		response.setContentType("Pragma : no-cache");
		
		// 전송 데이터가 stream 처리되도록 설정. 웹 상 전송 문자셋은 8859_1
		response.setContentType("application/octect-stream;charset=8859_1;");
		
		// 데이터 형식 성향 설정(attachment : 첨부파일)
		response.setHeader("Content-Disposition", value);
		
		// 내용물 인코딩 방식 결정
		response.setHeader("Content-Transfer-Encoding", "binary");
		
		// 파일 내보내기 - 정확하게는 이미지가 아닌 모든 파일임
		if (file.isFile()) {
			BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
			BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
			
			int i = 0;
			
			try {
				while((i=bis.read(b)) != -1) {
					bos.write(b, 0, i);
				}
			} catch (Exception e) {
			} finally {
				if (bos != null) bos.close();
				if (bis != null) bis.close();
			}
		}
		
	}
}

JSP

  1. 앨범 리스트 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<link rel="stylesheet" href="css/photo.css">
	<script src="js/HttpRequest.js"></script>
	<script type="text/javascript">
		function del(f) {
			var idx = f.idx.value.trim();
			var pwd = f.pwd.value.trim();
			var pwd2 = f.pwd2.value.trim();
			var filename = f.filename.value.trim();
			
			// 유효성 검사
			if (pwd2 == '') {
				alert('비밀번호를 입력해주세요');
				return;
			}
			if (pwd != pwd2) {
				alert('비밀번호가 일치하지 않습니다');
				return;
			}
			
			// 삭제 여부 체크
			if(!confirm('삭제하시겠습니까?')) {
				return;
			}
			
			var url = "delete";
			var param = "idx="+idx+"&filename="+filename;
			
			sendRequest(url, param, resultFn, "POST");
		}
		
		function resultFn() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				var data = xhr.responseText;
				
				var json = eval(data);
				
				if(json[0].param == "no") {
					alert('제거에 실패했습니다');
				} else {
					alert('성공적으로 제거했습니다');
				}
				location.href = "list";
			}
		}
		
		function download(filename){
			location.href = "download?dir=/upload/&filename="+encodeURIComponent(filename);	
		}
	</script>
</head>
<body>
	<div id="main_box">
		<h1> ::: Photo Gallery ::: </h1>
		
		<div align="center" style="margin:10px;">
			<input type="button" value="사진등록" onclick="location.href='photo_insert.jsp'">
		</div>
		<div id="photo_content">
			<c:forEach var="dto" items="${list}">
				<div class="photo_box">
					<img src="upload/${dto.filename}">
					<div class="title">
						<h2>${dto.title}</h2>
					</div>
					<form>
						<input type="hidden" name="idx" value="${dto.idx}">
						<input type="hidden" name="pwd" value="${dto.pwd}">
						<input type="hidden" name="filename" value="${dto.filename}">
						<div>
							<input type="password" name="pwd2" size="5">
							<input type="button" value="삭제" onclick="del(this.form)">
							<input type="button" value="다운로드" onclick="download('${dto.filename}')">
						</div>
					</form>
				</div>
			</c:forEach>
		</div>
	</div>
</body>
</html>
  1. 사진 등록 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script type="text/javascript">
		function send(f) {
			var title = f.title.value;
			var pwd = f.pwd.value;
			var photo = f.photo.value;
			
			// 유효성 검사
			if (title == '') {
				alert('제목을 입력하세요');
				return;
			}
			if (pwd == '') {
				alert('비밀번호를 입력하세요');
				return;
			}
			if (photo == '') {
				alert('파일을 업로드하세요');
				return;
			}
			
			f.submit();
		}
	</script>
</head>
<body>
	<form action="insert" method="POST" enctype="multipart/form-data">
		<table border="1" align="center">
			<caption>:: 사진 등록하기 ::</caption>
			<tr>
				<th>제목</th>
				<td><input name="title"></td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<th>등록할 사진</th>
				<td><input name="photo" type="file"></td>
			</tr>
			<tr>
				<td colspan="2" align="center">
					<input type="button" value="등록하기" onclick="send(this.form)">
					<input type="button" value="목록으로 돌아가기" onclick="location.href='list'">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. CSS
@charset "UTF-8";

*{margin:0; padding:0;}

#main_box{width: 900px; margin:0 auto;}

#main_box h1{
	text-align: center;
	text-shadow: 3px 3px 5px gray;
	color: ghostwhite;
}

#photo_content{
	margin:20px auto; padding:10px;
	width: 710px; height:665px;
	border:1px solid blue;
	overflow: auto;
	box-shadow: 5px 5px 10px gray;
}

.photo_box{
	margin:10px;
	width: 200px; height: auto;
	border: 1px solid green;
	float: left;
}

.photo_box h2{text-align: center;}

.photo_box img{
	display: block;
	margin:auto;
	width: 98%; height: 150px;
}

완성된 모습

  1. ListServlet에서 실행하면 앨범 사이트 화면이 뜬다. 아직 아무 사진도 업로드하지 않아 비어있다.
    앨범 1.png

  2. 사진 등록 버튼을 눌러 제목과 비밀번호, 파일을 선택해 업로드한다.
    앨범 2.png

  3. 파일이 정상적으로 업로드되어 사이트에 뜨는 것을 확인할 수 있다.
    앨범 3.png

  4. 서버(현재는 로컬 컴퓨터)의 실제 파일 경로에도 해당 파일이 있는 것을 확인할 수 있다.
    앨범 4.png

  5. 비밀번호 없이 삭제 버튼을 누르면 비밀번호 입력을 요구한다.
    앨범 5.png

  6. 비밀번호를 입력하고, 일치하는 비밀번호라면 정상적으로 삭제되었다는 메시지가 뜬다.
    앨범 6.png
    앨범 7.png

  7. 이미지가 삭제되어 화면에서 사라진다.
    앨범 8.png

  8. 다운로드 버튼을 누르면 파일을 다운로드할 수 있다.
    앨범 9.png

  9. 파일을 정상적으로 다운 받을 수 있다.
    앨범 10.png